Create UITextRange from NSRange
Asked Answered
H

7

56

I need to find the pixel-frame for different ranges in a textview. I'm using the - (CGRect)firstRectForRange:(UITextRange *)range; to do it. However I can't find out how to actually create a UITextRange.

Basically this is what I'm looking for:

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {

    UITextRange*range2 = [UITextRange rangeWithNSRange:range]; //DOES NOT EXIST 
    CGRect rect = [textView firstRectForRange:range2];
    return rect;
}

Apple says one has to subclass UITextRange and UITextPosition in order to adopt the UITextInput protocol. I don't do that, but I tried anyway, following the doc's example code and passing the subclass to firstRectForRange which resulted in crashing.

If there is a easier way of adding different colored UILables to a textview, please tell me. I have tried using UIWebView with content editable set to TRUE, but I'm not fond of communicating with JS, and coloring is the only thing I need.

Thanks in advance.

Hampshire answered 3/2, 2012 at 9:53 Comment(3)
You have to subclass UITextRange to be able to set it. The only way to set UITextRange properties is to access them in the subclass. It defines start and end as properties, but they can be set internally by referencing _start and _end.Quickly
Yes, but how do I then create a UITextPosition, which doesn't have any properties at all? 0.o If I subclass it as well, how could the 'firstRectForRange' know which property to use from my UITextPosition subclass?Hampshire
That is something that I don't know. All I know about this is that you have to subclass to set readonly properties. That is why this is a comment instead of an answer.Quickly
C
100

You can create a text range with the method textRangeFromPosition:toPosition. This method requires two positions, so you need to compute the positions for the start and the end of your range. That is done with the method positionFromPosition:offset, which returns a position from another position and a character offset.

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
    UITextPosition *beginning = textView.beginningOfDocument;
    UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [textView positionFromPosition:start offset:range.length];
    UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
    CGRect rect = [textView firstRectForRange:textRange];
    return [textView convertRect:rect fromView:textView.textInputView];
}
Collimator answered 7/3, 2012 at 17:36 Comment(7)
I have a small remark. This only works if the TextField is the first responder. Otherwise all positions will be NULL and the rect has a zero size respectively. If anybody has a hint how one can get the rect without the TextField being the first responder, please share it with us.Suppuration
Awesome! Helped me a lot. @Suppuration for me this is working without the UITextView being the first responder (I have a read-only UITextView inside a UITableViewCell).Conah
I have found the same issue as Thomas - textView.beginningOfDocument is nil if textView is not the first responder. Since everything else in this example, which works great by the way, is based off beginningOfDocument, all your calculations end up being sent to nil objects.Admissible
text system is WAY over engineeredSmirch
This is great, but can anyone indicate how I would get the rect of just the VISIBLE portion of this. As an example if the range is an image attachment with the top portion of the image not visible (scrolled above top border of UITextView). Is there an easy way to get the intersection (?) of the above CGRect and the UITextView's visible rect ?Tutorial
I had a similar problem with not getting a value for beginningOfDocument - In my case, I had the UITextView as selectable = NO. Changing this to YES was required...Advisable
This works, but only if the textView/Field is the firstResponder (currently editing it), otherwise beginningOfDocument will be nil. As a workaround, set it as the first responder then resignFirstResponder ASAP. They keyboard won't flash in the UI.Hippocrates
T
17

It is a bit ridiculous that seems to be so complicated. A simple "workaround" would be to select the range (accepts NSRange) and then read the selectedTextRange (returns UITextRange):

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
    textView.selectedRange = range;
    UITextRange *textRange = [textView selectedTextRange]; 
    CGRect rect = [textView firstRectForRange:textRange];
    return rect;
}

This worked for me even if the textView is not first responder.


If you don't want the selection to persist, you can either reset the selectedRange:

textView.selectedRange = NSMakeRange(0, 0);

...or save the current selection and restore it afterwards

NSRange oldRange = textView.selectedRange;
// do something
// then check if the range is still valid and
textView.selectedRange = oldRange;
Trepidation answered 5/10, 2013 at 20:13 Comment(3)
in iOS 7 the textview must be selectable, otherwise selectedTextRange: returns nilPhototelegraphy
That is so ridiculously simple... and even if it's not selectable, it still works on iOS 7.Delija
It's always better to let the SDK todo the work for you. Good Job!Swage
O
12

Swift 4 of Andrew Schreiber's answer for easy copy/paste

extension NSRange {
    func toTextRange(textInput:UITextInput) -> UITextRange? {
        if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location),
            let rangeEnd = textInput.position(from: rangeStart, offset: length) {
            return textInput.textRange(from: rangeStart, to: rangeEnd)
        }
        return nil
    }
}
Olsson answered 17/9, 2017 at 21:8 Comment(0)
T
6

To the title question, here is a Swift 2 extension that creates a UITextRange from an NSRange.

The only initializer for UITextRange is a instance method on the UITextInput protocol, thus the extension also requires you pass in UITextInput such as UITextField or UITextView.

extension NSRange {
    func toTextRange(textInput textInput:UITextInput) -> UITextRange? {
        if let rangeStart = textInput.positionFromPosition(textInput.beginningOfDocument, offset: location),
            rangeEnd = textInput.positionFromPosition(rangeStart, offset: length) {
            return textInput.textRangeFromPosition(rangeStart, toPosition: rangeEnd)
        }
        return nil
    }
}
Thebaid answered 18/5, 2016 at 19:53 Comment(0)
N
0

Swift 4 of Nicolas Bachschmidt's answer as an UITextView extension using swifty Range<String.Index> instead of NSRange:

extension UITextView {
    func frame(ofTextRange range: Range<String.Index>?) -> CGRect? {
        guard let range = range else { return nil }
        let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
        guard
            let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
            let end = position(from: start, offset: length),
            let txtRange = textRange(from: start, to: end)
            else { return nil }
        let rect = self.firstRect(for: txtRange)
        return self.convert(rect, to: textInputView)
    }
}

Possible use:

guard let rect = textView.frame(ofTextRange: text.range(of: "awesome")) else { return }
let awesomeView = UIView()
awesomeView.frame = rect.insetBy(dx: -3.0, dy: 0)
awesomeView.layer.borderColor = UIColor.black.cgColor
awesomeView.layer.borderWidth = 1.0
awesomeView.layer.cornerRadius = 3
self.view.insertSubview(awesomeView, belowSubview: textView)
Naumachia answered 9/3, 2018 at 15:46 Comment(0)
L
0
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
    UITextRange *textRange = [[textView _inputController] _textRangeFromNSRange:range]; // Private
    CGRect rect = [textView firstRectForRange:textRange];
    return rect;
}
Latashialatch answered 16/6, 2022 at 13:42 Comment(0)
H
-1

Here is explain.

A UITextRange object represents a range of characters in a text container; in other words, it identifies a starting index and an ending index in string backing a text-entry object.

Classes that adopt the UITextInput protocol must create custom UITextRange objects for representing ranges within the text managed by the class. The starting and ending indexes of the range are represented by UITextPosition objects. The text system uses both UITextRange and UITextPosition objects for communicating text-layout information. There are two reasons for using objects for text ranges rather than primitive types such as NSRange:

Some documents contain nested elements (for example, HTML tags and embedded objects) and you need to track both absolute position and position in the visible text.

The WebKit framework, which the iPhone text system is based on, requires that text indexes and offsets be represented by objects.

If you adopt the UITextInput protocol, you must create a custom UITextRange subclass as well as a custom UITextPosition subclass.

For example like in those sources

Heartbeat answered 24/9, 2014 at 9:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.